package com.duomn.mybatis.plugins;

import java.lang.reflect.Method;
import java.sql.Connection;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.Properties;

import org.apache.ibatis.builder.xml.dynamic.ForEachSqlNode;
import org.apache.ibatis.cache.CacheKey;
import org.apache.ibatis.executor.CachingExecutor;
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.executor.parameter.ParameterHandler;
import org.apache.ibatis.executor.statement.BaseStatementHandler;
import org.apache.ibatis.executor.statement.RoutingStatementHandler;
import org.apache.ibatis.executor.statement.StatementHandler;
import org.apache.ibatis.logging.Log;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.ParameterMapping;
import org.apache.ibatis.mapping.ParameterMode;
import org.apache.ibatis.plugin.Interceptor;
import org.apache.ibatis.plugin.Intercepts;
import org.apache.ibatis.plugin.Invocation;
import org.apache.ibatis.plugin.Plugin;
import org.apache.ibatis.plugin.Signature;
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.reflection.property.PropertyTokenizer;
import org.apache.ibatis.session.Configuration;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.duomn.mybatis.plugins.model.ExecuteMode;
import com.duomn.mybatis.plugins.model.ParsedStatement;
import com.duomn.mybatis.plugins.parse.SQLParserImpl;
import com.duomn.mybatis.plugins.tools.ShareContext;
import com.duomn.mybatis.util.ReflectUtil;

@Intercepts(value = { 
		@Signature(args = { MappedStatement.class, Object.class}, method = "update", type = Executor.class),
		@Signature(args = { MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class, CacheKey.class, BoundSql.class}, method = "query", type = Executor.class),
		@Signature(args = { MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}, method = "query", type = Executor.class),
		@Signature(args = { Connection.class }, method = "prepare", type = StatementHandler.class), 
		@Signature(args = { Statement.class }, method = "parameterize", type = StatementHandler.class), 
		@Signature(args = { Statement.class, ResultHandler.class }, method = "query", type = StatementHandler.class),
		@Signature(args = { Statement.class}, method = "update", type = StatementHandler.class),
		})
public class StatementPlugin implements Interceptor {
	
	private static Logger logger = LoggerFactory.getLogger(StatementPlugin.class);

	@Override
	public Object intercept(Invocation invocation) throws Throwable {
		Object obj = invocation.getTarget();
		if (obj instanceof Executor) { // 先进来的方法，套一层
			try {
				ShareContext.increaeDep();
				return invocation.proceed();
			} finally {
				ShareContext.decreaeDep();
				if (ShareContext.isEmpty()) {
					ShareContext.clear();
				}
			}
		}
		
		// 只对最外层，且需要分表的进行拦截
		if (!ShareContext.isBottom() || !ShareContext.isSharding()) { 
			return invocation.proceed();
		}
		
		try {
			Method method = invocation.getMethod();
			if ("prepare".equals(method.getName())) {
				RoutingStatementHandler target = (RoutingStatementHandler) invocation.getTarget();
				// BaseStatementHandler 可以获取所有的属性
				BaseStatementHandler handler = (BaseStatementHandler) ReflectUtil.getFieldValue(target, "delegate");
				
				// BaseStatementHandler(boundSql)
				BoundSql boundSql = handler.getBoundSql();
				String sql = (String) ReflectUtil.getFieldValue(boundSql, "sql");
				
				// BaseStatementHandler(parameterHandler)
				ParameterHandler parameterHandler = handler.getParameterHandler();
				Object parameterObject = parameterHandler.getParameterObject();
				
				
				// executor （用于重新创建一个connection）
				Executor executor = (Executor) ReflectUtil.getFieldValue(handler, "executor");
				if (executor instanceof CachingExecutor) {
					executor =  (Executor) ReflectUtil.getFieldValue(executor, "delegate");
				}
				// mappedStatement （用于获取创建 connection时的传入参数）
				MappedStatement ms = (MappedStatement) ReflectUtil.getFieldValue(handler, "mappedStatement");
				
				// Configure
				Configuration configuration = ms.getConfiguration();
				
				List<Object> params = parseBoundSql(boundSql, parameterObject, configuration);
				
				// 转换sql
				ISqlParser sp = new SQLParserImpl(sql.trim().toLowerCase(), params);
				ParsedStatement parsedSt = sp.getParsedStatement();
				
				ShareContext.setExecuteMode(parsedSt.getMode());
				if (!parsedSt.isSharding()) { // 如果不需要分表
					ShareContext.setSharding(false);
					return invocation.proceed();
				}
				
				List<String> sqls = parsedSt.getSqls();
				for (String newSql : sqls) {  // 
					ReflectUtil.setFieldValue(boundSql, "sql", newSql);
					Connection conn = (Connection) ReflectUtil.invokeMethod("getConnection", executor, Log.class, ms.getStatementLog());
					Statement statement = (Statement) invocation.getMethod().invoke(invocation.getTarget(), conn);
					logger.debug("sql[" + newSql.replaceAll("([ \t\n\r\b]{1,})", " ") + "]");
					ShareContext.put(statement);
				}
			} else if ("parameterize".equals(method.getName())) {
				for (Statement statement : ShareContext.get()) {
					method.invoke(invocation.getTarget(), statement);
				}
			} else if ("query".equals(method.getName())) {
				ExecuteMode mode = ShareContext.getExecuteMode();
				List<Object> result = new LinkedList<Object>();
				Object[] args = invocation.getArgs();
				for (Statement statement : ShareContext.get()) {
					args[0] = statement;
					List<?> rst = (List<?>) method.invoke(invocation.getTarget(), args);
					if (mode == ExecuteMode.EXE_ONLY) { // 执行一条均可，无论是否有结果
						if (rst != null && rst.size() > 0) {
							result.addAll(rst);
						}
						break;
					} else if (mode == ExecuteMode.EXE_ANY) { // 执行一条，有结果的
						if (rst != null && rst.size() > 0) {
							result.addAll(rst);
							break;
						}
					} else { // 合并所有有结果的
						if (rst != null && rst.size() > 0) {
							result.addAll(rst);
						}
					}
				}
				return result;
			} else if ("update".equals(method.getName())) {
				ExecuteMode mode = ShareContext.getExecuteMode();
				int update = 0;
				for (Statement statement : ShareContext.get()) {
					if (mode == ExecuteMode.EXE_ONLY) {
						int change =  (Integer) method.invoke(invocation.getTarget(), statement);
						update += change;
						break;
					} else if (mode == ExecuteMode.EXE_ANY) {
						int change =  (Integer) method.invoke(invocation.getTarget(), statement);
						update += change;
						if (change != 0) {
							break;
						}
					} else {
						int change =  (Integer) method.invoke(invocation.getTarget(), statement);
						update += change;
					}
				}
				return update;
			} else {
				System.err.println("发生错误");
			}
		} catch (Exception e) {
			ShareContext.clear();
			throw e;
		}
		return null;
	}

	private List<Object> parseBoundSql(BoundSql boundSql, Object parameterObject, Configuration configuration) {
		List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
		List<Object> paramList = new ArrayList<Object>(parameterMappings.size());
		if (parameterMappings != null) {
			MetaObject metaObject = parameterObject == null ? null : configuration.newMetaObject(parameterObject);
			for (int i = 0; i < parameterMappings.size(); i++) {
				ParameterMapping parameterMapping = parameterMappings.get(i);
				if (parameterMapping.getMode() != ParameterMode.OUT) {
					Object value = null;
					String propertyName = parameterMapping.getProperty();
					PropertyTokenizer prop = new PropertyTokenizer(propertyName);
					if (parameterObject == null) {
						value = null;
					} else if (configuration.getTypeHandlerRegistry().hasTypeHandler(parameterObject.getClass())) {
						value = parameterObject;
					} else if (boundSql.hasAdditionalParameter(propertyName)) {
						value = boundSql.getAdditionalParameter(propertyName);
					} else if (propertyName.startsWith(ForEachSqlNode.ITEM_PREFIX) && boundSql.hasAdditionalParameter(prop.getName())) {
						value = boundSql.getAdditionalParameter(prop.getName());
						if (value != null) {
							value = configuration.newMetaObject(value).getValue(propertyName.substring(prop.getName().length()));
						}
					} else {
						value = metaObject == null ? null : metaObject.getValue(propertyName);
					}
					paramList.add(value);
				}
			}
		}
		return paramList;
	}
	
	@Override
	public Object plugin(Object target) {
		if (target instanceof Executor || target instanceof StatementHandler) {
			return Plugin.wrap(target, this);
		}
		return target;
	}

	@Override
	public void setProperties(Properties properties) {
		
	}

}
